# frozen_string_literal: true

require_relative '../../../spec_helper'
require 'readline'
require 'wpxf/modules'
require 'wpxf/cli/context'
require 'wpxf/cli/auto_complete'

describe Wpxf::Cli::AutoComplete do
  let :subject do
    Class.new do
      include Wpxf::Cli::AutoComplete
    end.new
  end

  before :each, 'setup subject' do
    allow(subject).to receive(:context).and_return(nil)

    Wpxf::Models::Module.create(
      path: 'exploit/shell/admin_shell_upload',
      type: 'exploit',
      name: 'Admin Shell Upload',
      class_name: 'Wpxf::Exploit::AdminShellUpload'
    )

    Wpxf::Models::Module.create(
      path: 'auxiliary/dos/load_scripts_dos',
      type: 'auxiliary',
      name: 'WordPress "load-scripts.php" DoS',
      class_name: 'Wpxf::Auxiliary::LoadScriptsDos'
    )
  end

  describe '#setup_auto_complete' do
    before :each, 'setup mocks' do
      allow(subject).to receive(:permitted_commands).and_return(%w[use show])

      allow(Readline).to receive(:completer_word_break_characters=).and_call_original
      allow(Readline).to receive(:completion_append_character=).and_call_original
      allow(Readline).to receive(:completion_proc=).and_call_original
    end

    it 'should initialise #autocomplete_list' do
      expect(subject.autocomplete_list).to be_nil
      subject.setup_auto_complete
      expect(subject.autocomplete_list).to_not be_nil
      expect(subject.autocomplete_list).to include('use')
      expect(subject.autocomplete_list).to include('show')
    end

    it 'should configure the Readline word break characters' do
      subject.setup_auto_complete
      expect(Readline).to have_received(:completer_word_break_characters=)
        .with('')
        .exactly(1)
        .times
    end

    it 'should configure the Readline append character' do
      subject.setup_auto_complete
      expect(Readline).to have_received(:completion_append_character=)
        .with(' ')
        .exactly(1)
        .times
    end

    it 'should setup a custom completion proc for Readline' do
      subject.setup_auto_complete
      custom_proc = subject.method(:readline_completion_proc)
      expect(Readline).to have_received(:completion_proc=)
        .with(custom_proc)
        .exactly(1)
        .times
    end
  end

  describe '#readline_completion_proc' do
    before :each, 'setup mocks' do
      allow(subject).to receive(:permitted_commands).and_return(%w[use show])
    end

    it 'returns an array of auto-completion options' do
      allow(subject).to receive(:auto_complete_proc).and_return(%w[a b])

      subject.setup_auto_complete
      res = subject.readline_completion_proc('test')
      expect(res).to eql %w[a b]
    end

    context 'if no auto-completion options are available' do
      it 'returns an empty array' do
        allow(subject).to receive(:auto_complete_proc).and_return(nil)
        subject.setup_auto_complete
        res = subject.readline_completion_proc('test')
        expect(res).to eql []
      end
    end
  end

  describe '#refresh_autocomplete_options' do
    before :each, 'setup mocks' do
      allow(subject).to receive(:permitted_commands).and_return(%w[use show])
    end

    context 'if a module is loaded' do
      let(:context) { Wpxf::Cli::Context.new }

      before :each, 'run subject' do
        context.load_module 'exploit/shell/admin_shell_upload'
        allow(subject).to receive(:context).and_return(context)

        subject.setup_auto_complete
        subject.refresh_autocomplete_options
      end

      it 'should set module options as auto-completions for `set`' do
        opts = subject.autocomplete_list['set']
        expect(opts).to include('username')
        expect(opts).to include('password')
        expect(opts).to include('host')
      end

      it 'should set module options as auto-completions for `unset`' do
        opts = subject.autocomplete_list['unset']
        expect(opts).to include('username')
        expect(opts).to include('password')
        expect(opts).to include('host')
      end

      it 'should set module options as auto-completions for `gset`' do
        opts = subject.autocomplete_list['gset']
        expect(opts).to include('username')
        expect(opts).to include('password')
        expect(opts).to include('host')
      end

      it 'should set module options as auto-completions for `gunset`' do
        opts = subject.autocomplete_list['gunset']
        expect(opts).to include('username')
        expect(opts).to include('password')
        expect(opts).to include('host')
      end

      it 'should set module options as auto-completions for `setg`' do
        opts = subject.autocomplete_list['setg']
        expect(opts).to include('username')
        expect(opts).to include('password')
        expect(opts).to include('host')
      end

      it 'should set module options as auto-completions for `unsetg`' do
        opts = subject.autocomplete_list['unsetg']
        expect(opts).to include('username')
        expect(opts).to include('password')
        expect(opts).to include('host')
      end

      context 'if a payload is loaded' do
        before :each, 'load payload' do
          context.load_payload 'reverse_tcp'
          subject.refresh_autocomplete_options
        end

        it 'should set module and payload options as auto-completions for `set`' do
          opts = subject.autocomplete_list['set']
          expect(opts).to include('username')
          expect(opts).to include('password')
          expect(opts).to include('host')
          expect(opts).to include('shell')
          expect(opts).to include('lhost')
          expect(opts).to include('lport')
        end

        it 'should set module and payload options as auto-completions for `unset`' do
          opts = subject.autocomplete_list['unset']
          expect(opts).to include('username')
          expect(opts).to include('password')
          expect(opts).to include('host')
          expect(opts).to include('shell')
          expect(opts).to include('lhost')
          expect(opts).to include('lport')
        end

        it 'should set module and payload options as auto-completions for `gset`' do
          opts = subject.autocomplete_list['gset']
          expect(opts).to include('username')
          expect(opts).to include('password')
          expect(opts).to include('host')
          expect(opts).to include('shell')
          expect(opts).to include('lhost')
          expect(opts).to include('lport')
        end

        it 'should set module and payload options as auto-completions for `gunset`' do
          opts = subject.autocomplete_list['gunset']
          expect(opts).to include('username')
          expect(opts).to include('password')
          expect(opts).to include('host')
          expect(opts).to include('shell')
          expect(opts).to include('lhost')
          expect(opts).to include('lport')
        end

        it 'should set module and payload options as auto-completions for `setg`' do
          opts = subject.autocomplete_list['setg']
          expect(opts).to include('username')
          expect(opts).to include('password')
          expect(opts).to include('host')
          expect(opts).to include('shell')
          expect(opts).to include('lhost')
          expect(opts).to include('lport')
        end

        it 'should set module and payload options as auto-completions for `unsetg`' do
          opts = subject.autocomplete_list['unsetg']
          expect(opts).to include('username')
          expect(opts).to include('password')
          expect(opts).to include('host')
          expect(opts).to include('shell')
          expect(opts).to include('lhost')
          expect(opts).to include('lport')
        end
      end

      context 'if the module is an exploit' do
        it 'should add a `payload` auto-complete option for `set`' do
          opts = subject.autocomplete_list['set']
          expect(opts).to include('payload')
        end

        it 'should include payload names as an auto-complete option for `set payload`' do
          opts = subject.autocomplete_list['set']['payload']
          expect(opts).to include('reverse_tcp')
          expect(opts).to include('exec')
          expect(opts).to include('download_exec')
        end

        it 'should add a `payload` auto-complete option for `unset`' do
          opts = subject.autocomplete_list['unset']
          expect(opts).to include('payload')
        end
      end

      context 'if the module is an auxiliary' do
        before :each, 'setup subject' do
          context.load_module 'auxiliary/dos/load_scripts_dos'
          subject.setup_auto_complete
          subject.refresh_autocomplete_options
        end

        it 'should not add a `payload` auto-complete option for `set`' do
          opts = subject.autocomplete_list['set']
          expect(opts).to_not include('payload')
        end

        it 'should not add a `payload` auto-complete option for `unset`' do
          opts = subject.autocomplete_list['unset']
          expect(opts).to_not include('payload')
        end
      end
    end

    context 'if no module is loaded' do
      before :each, 'run subject' do
        subject.setup_auto_complete
        subject.refresh_autocomplete_options
      end

      it 'should have no auto-completions for `set`' do
        expect(subject.autocomplete_list['set']).to eql({})
      end

      it 'should have no auto-completions for `unset`' do
        expect(subject.autocomplete_list['unset']).to eql({})
      end

      it 'should have no auto-completions for `gset`' do
        expect(subject.autocomplete_list['gset']).to eql({})
      end

      it 'should have no auto-completions for `gunset`' do
        expect(subject.autocomplete_list['gunset']).to eql({})
      end

      it 'should have no auto-completions for `setg`' do
        expect(subject.autocomplete_list['setg']).to eql({})
      end

      it 'should have no auto-completions for `unsetg`' do
        expect(subject.autocomplete_list['unsetg']).to eql({})
      end
    end
  end

  describe '#build_cmd_list' do
    before :each, 'setup mocks' do
      allow(subject).to receive(:permitted_commands).and_return(%w[use show])

      Wpxf::Models::Module.create(path: '/test1', name: 'Test 1', type: 'exploit', class_name: 'Test1')
      Wpxf::Models::Module.create(path: '/test2', name: 'Test 2 - Plugin', type: 'auxiliary', class_name: 'Test2')
      Wpxf::Models::Module.create(path: '/test3', name: 'TestPlugin 3', type: 'exploit', class_name: 'Test3')
      Wpxf::Models::Module.create(path: '/test4', name: 'Test 4', type: 'exploit', class_name: 'Test4')
      Wpxf::Models::Module.create(path: '/test5', name: 'Test PLUGIN 5', type: 'exploit', class_name: 'Test5')
    end

    it 'should include all #permitted_commands in the returned hash' do
      list = subject.build_cmd_list
      expect(list).to include('use')
      expect(list).to include('show')
    end

    it 'should add an `options` suggestion to `show`' do
      list = subject.build_cmd_list['show']
      expect(list).to include('options')
    end

    it 'should add an `exploits` suggestion to `show`' do
      list = subject.build_cmd_list['show']
      expect(list).to include('exploits')
    end

    it 'should add an `advanced` suggestion to `show`' do
      list = subject.build_cmd_list['show']
      expect(list).to include('advanced')
    end

    it 'should add an `auxiliary` suggestion to `show`' do
      list = subject.build_cmd_list['show']
      expect(list).to include('auxiliary')
    end

    it 'should add module paths as suggestions to `use`' do
      list = subject.build_cmd_list['use']
      expect(list).to include('/test1')
      expect(list).to include('/test2')
      expect(list).to include('/test3')
      expect(list).to include('/test4')
      expect(list).to include('/test5')
    end
  end

  describe '#auto_complete_proc' do
    let(:autocomplete_list) do
      {
        'exit' => {},
        'search' => {},
        'set' => {
          'opt1' => {},
          'opt2' => {}
        },
        'show' => {
          'options' => {},
          'exploits' => {}
        }
      }
    end

    context 'if `list` has no keys' do
      it 'should return nil' do
        res = subject.auto_complete_proc 's', {}
        expect(res).to be_nil
      end
    end

    context 'if `input` has only one word' do
      it 'should return suggestions only from the top level' do
        res = subject.auto_complete_proc 's', autocomplete_list
        expect(res).to eql %w[search set show]
      end
    end

    context 'if `input` contains a trailing space' do
      context 'and the last word has further child-suggestions' do
        it 'should return the full path to the suggestions' do
          res = subject.auto_complete_proc 'show ', autocomplete_list
          expect(res).to eql ['show options', 'show exploits']
        end
      end

      context 'and the last word has no more child-suggestions' do
        it 'should return an empty array' do
          res = subject.auto_complete_proc 'search ', autocomplete_list
          expect(res).to eql []
        end
      end
    end

    context 'if `input` contains a a partial match on a level deeper than the top level of suggestions' do
      it 'should descend into the next level and return the matching suggestions' do
        res = subject.auto_complete_proc 'show o', autocomplete_list
        expect(res).to eql ['show options']
      end
    end
  end
end
